﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.Text;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;
using System.IO;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Digests;
using System.Security.Cryptography;
using Microsoft.Phone.Net.NetworkInformation;
using System.Windows.Navigation;
using System.IO.IsolatedStorage;

namespace BattleAuth
{
    // See the Battle.net Authenticator Specfiction here: http://www.wowpedia.org/Battle.net_Mobile_Authenticator_Specification
    public class BattleAuthenticator
    {
        #region Public Methods
        public BattleAuthenticator()
        {

        }
        /// <summary>
        /// Load existing settings
        /// </summary>
        /// <param name="loadSettings">Set to True to load existing settings.</param>
        public BattleAuthenticator(bool loadSettings)
        {
            if ((loadSettings == true) && (HasSettings() == true))
            {
                this.LoadSettings();
            }
        }
        /// <summary>
        /// Get a new autenticator code
        /// </summary>
        /// <returns>Returns an 8 digit authenticator code</returns>
        public string GetCode()
        {
            // Synchronise time if the Server Time Difference is 0
            if (this.ServerTimeDifference == 0)
            {
                Synchronise();
            }

            HMac m_hmac = new HMac(new Sha1Digest());
            m_hmac.Init(new KeyParameter(SecretKey));

            byte[] m_intervalArray = BitConverter.GetBytes(Interval);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(m_intervalArray);
            }
            m_hmac.BlockUpdate(m_intervalArray, 0, m_intervalArray.Length);

            byte[] mac = new byte[m_hmac.GetMacSize()];
            m_hmac.DoFinal(mac, 0);

            // Last 4 bytes denote where the code begins.
            int start = mac[19] & 0x0f;

            // Extract the 4 bytes
            byte[] bytes = new byte[4];
            Array.Copy(mac, start, bytes, 0, 4);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(bytes);
            }
            uint _authCode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff;

            // Return the 8 digits of the code
            string authenticatorCode = (_authCode % 100000000).ToString("00000000");

            return authenticatorCode;
        }
        #endregion

        #region Enroll
        public bool IsEnrolling = false;
        public void Enroll()
        {
            // Verify the device has an internet connection available
            if (InternetIsAvailable() == false)
            {
                return;
            }
            // Set IsEnrolling to true, this allows the UI to display a progress screen while the asynchronous call to Battle.net is processed.
            IsEnrolling = true;

            // Create an asynchronous connection to the Enrollment service.
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://m.us.mobileservice.blizzard.com/enrollment/enroll.htm");
            request.Method = "POST";
            request.ContentType = "application/octet-stream";
            request.BeginGetRequestStream(RequestCallback, request);
        }
        void RequestCallback(IAsyncResult result)
        {
            HttpWebRequest request = result.AsyncState as HttpWebRequest;
            byte[] bytes = new byte[56];
            int index = 0;
            byte[] code = new byte[] { 1 };
            code.CopyTo(bytes, index);
            index += code.Length;
            key = CreateRandomByteArray();
            key.CopyTo(bytes, index);
            index += key.Length;
            byte[] regionarray = Encoding.UTF8.GetBytes("US");
            regionarray.CopyTo(bytes, index);
            index += regionarray.Length;
            byte[] model = Encoding.UTF8.GetBytes(CreateRandomModelNumber());
            model.CopyTo(bytes, index);
            index += model.Length;

            // Using Bouncy Castle library. Windows Phone 7 lacks many cryptography APIs
            RsaEngine rsa = new RsaEngine();
            rsa.Init(true, new RsaKeyParameters(false, new Org.BouncyCastle.Math.BigInteger(MODULUS, 16), new Org.BouncyCastle.Math.BigInteger(EXPONENT, 16)));
            byte[] requestData = rsa.ProcessBlock(bytes, 0, bytes.Length);

            Stream requestStream = request.EndGetRequestStream(result);
            requestStream.Write(requestData, 0, requestData.Length);
            requestStream.Close();
            // Start Async operation to get the response data
            request.BeginGetResponse(ResponseCallback, request);
        }
        private byte[] key = null;
        void ResponseCallback(IAsyncResult result)
        {
            HttpWebRequest request = result.AsyncState as HttpWebRequest;
            HttpWebResponse response = request.EndGetResponse(result) as HttpWebResponse;
            byte[] responseData = null;
            if (response.StatusCode != HttpStatusCode.OK)
            {
                throw new Exception("Response code returned was " + response.StatusCode + " - Expected 'OK'");
            }
            using (MemoryStream ms = new MemoryStream())
            {
                using (Stream bs = response.GetResponseStream())
                {
                    byte[] temp = new byte[64];
                    int read;
                    while ((read = bs.Read(temp, 0, 64)) != 0)
                    {
                        ms.Write(temp, 0, read);
                    }
                    responseData = ms.ToArray();

                    // Verify the Responsa Data's length is 45
                    if (responseData.Length != 45)
                    {
                        throw new Exception("The response data's length was not 45.");
                    }
                }
            }
            byte[] serverTime = new byte[8];
            Array.Copy(responseData, serverTime, 8);
            if (BitConverter.IsLittleEndian == true)
            {
                Array.Reverse(serverTime);
            }
            // Get the time difference between Battle.net and the device
            long serverTimeDiff = BitConverter.ToInt64(serverTime, 0) - CurrentTime;
            byte[] initData = new byte[37];
            Array.Copy(responseData, 8, initData, 0, 37);
            for (int i = 0; i < initData.Length; i++)
            {
                initData[i] ^= key[i];
            }
            byte[] secretKey = new byte[20];
            Array.Copy(initData, secretKey, 20);
            string serial = Encoding.UTF8.GetString(initData, 20, 17);
            // Set the properties the UI will use to display the information
            SecretKey = secretKey;
            Serial = serial;
            ServerTimeDifference = serverTimeDiff;
            // Save the properties into the Isolated Storage area.
            SaveSettings();
            // Let the UI know the enrollment is now done and to hide the progress screen.
            IsEnrolling = false;
        }
        #endregion

        #region Synchronise
        /// <summary>
        /// Synchronised this authenticator's time with server time. UTC Time
        /// </summary>
        public void Synchronise()
        {
            if (InternetIsAvailable() == false)
            {
                MessageBox.Show("Unable to connect to Battle.net", "Sync Unavailable", MessageBoxButton.OK);
                return;
            }
            // Create an asynchronous connection to the Time service.
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://m.us.mobileservice.blizzard.com/enrollment/time.htm");
            request.Method = "GET";
            request.BeginGetResponse(SyncResponseCallback, request);
        }
        void SyncResponseCallback(IAsyncResult result)
        {
            try
            {
                HttpWebRequest request = result.AsyncState as HttpWebRequest;
                HttpWebResponse response = request.EndGetResponse(result) as HttpWebResponse;
                byte[] responseData = null;
                if (response.StatusCode != HttpStatusCode.OK)
                {
                    throw new Exception(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription));
                }
                using (MemoryStream ms = new MemoryStream())
                {
                    using (Stream bs = response.GetResponseStream())
                    {
                        byte[] temp = new byte[64];
                        int read;
                        while ((read = bs.Read(temp, 0, 64)) != 0)
                        {
                            ms.Write(temp, 0, read);
                        }
                        responseData = ms.ToArray();

                        // Verify the Responsa Data's length is 8
                        if (responseData.Length != 8)
                        {
                            throw new Exception("The response data's length was not 8.");
                        }
                    }
                }
                if (BitConverter.IsLittleEndian == true)
                {
                    Array.Reverse(responseData);
                }
                long serverTimeDiff = BitConverter.ToInt64(responseData, 0) - CurrentTime;
                ServerTimeDifference = serverTimeDiff;
            }
            catch (Exception ex)
            {
                System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
                {
                    MessageBox.Show("An error occured while synchronising with Battle.net", "Sync Error", MessageBoxButton.OK);
                });
            }
        }
        #endregion

        #region Properties
        public long Interval
        {
            get
            {
                return (CurrentTime + ServerTimeDifference) / 30000L;
            }
        }
        public static long CurrentTime
        {
            get
            {
                return Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds);
            }
        }
        private const string MODULUS = "955e4bd989f3917d2f15544a7e0504eb9d7bb66b6f8a2fe470e453c779200e5e3ad2e43a02d06c4adbd8d328f1a426b83658e88bfd949b2af4eaf30054673a1419a250fa4cc1278d12855b5b25818d162c6e6ee2ab4a350d401d78f6ddb99711e72626b48bd8b5b0b7f3acf9ea3c9e0005fee59e19136cdb7c83f2ab8b0a2a99";
        private const string EXPONENT = "0101";
        private static byte[] BMA_KEY = new byte[]
		{
			0x39,0x8e,0x27,0xfc,0x50,0x27,0x6a,0x65,0x60,0x65,0xb0,0xe5,0x25,0xf4,0xc0,0x6c,
			0x04,0xc6,0x10,0x75,0x28,0x6b,0x8e,0x7a,0xed,0xa5,0x9d,0xa9,0x81,0x3b,0x5d,0xd6,
			0xc8,0x0d,0x2f,0xb3,0x80,0x68,0x77,0x3f,0xa5,0x9b,0xa4,0x7c,0x17,0xca,0x6c,0x64,
			0x79,0x01,0x5c,0x1d,0x5b,0x8b,0x8f,0x6b,0x9a
		};

        public byte[] SecretKey { get; set; }
        public string Serial { get; set; }
        public long ServerTimeDifference { get; set; }
        public long ServerTime
        {
            get
            {
                return CurrentTime + ServerTimeDifference;
            }
        }
        public string SecretData
        {
            get
            {
                string code = BattleAuthenticator.ConvertToString(SecretKey) + Serial;
                byte[] plain = Encoding.UTF8.GetBytes(code);
                for (int i = plain.Length - 1; i >= 0; i--)
                {
                    plain[i] ^= BMA_KEY[i];
                }
                return BattleAuthenticator.ConvertToString(plain);
            }
            set
            {
                byte[] bytes = BattleAuthenticator.ConvertToByteArray(value);
                for (int i = bytes.Length - 1; i >= 0; i--)
                {
                    bytes[i] ^= BMA_KEY[i];
                }
                string full = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
                SecretKey = BattleAuthenticator.ConvertToByteArray(full.Substring(0, 40));
                Serial = full.Substring(40);
            }
        }
        #endregion

        #region Settings
        /// <summary>
        /// Check whether there are settings already in storage
        /// </summary>
        /// <returns>Returns a boolean value of true if there are settings already stored.</returns>
        public static bool HasSettings()
        {
            try
            {
                var settings = IsolatedStorageSettings.ApplicationSettings;
                if (settings["SecretData"] != null)
                {
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                return false;
            }
        }
        /// <summary>
        /// Load settings. Windows Phone 7 limits storage IsolatedStorageSettings.
        /// </summary>
        public void LoadSettings()
        {
            var settings = IsolatedStorageSettings.ApplicationSettings;
            ServerTimeDifference = (long)settings["ServerTimeDifference"];
            Serial = (string)settings["Serial"];
            SecretKey = (byte[])settings["SecretKey"];
            SecretData = (string)settings["SecretData"];
        }
        /// <summary>
        /// Save settings. Windows Phone 7 limits storage IsolatedStorageSettings.
        /// </summary>
        public void SaveSettings()
        {
            var settings = IsolatedStorageSettings.ApplicationSettings;
            settings.Clear();
            settings.Add("ServerTimeDifference", ServerTimeDifference);
            settings.Add("Serial", Serial);
            settings.Add("SecretKey", SecretKey);
            settings.Add("SecretData", SecretData);
        }
        #endregion

        #region Helper Methods
        /// <summary>
        /// Checks for an internet connection. Windows Phone emulator will always return true.
        /// </summary>
        /// <returns></returns>
        public static bool InternetIsAvailable()
        {
            var available = NetworkInterface.GetIsNetworkAvailable();
            //#if DEBUG
            //available = false;
            //#endif
            if (!available)
            {
                return false;
            }
            return true;
        }
        protected static byte[] CreateRandomByteArray()
        {
            byte[] hashBlock = new byte[128];
            RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();
            random.GetBytes(hashBlock);

            SHA1 sha1 = new SHA1Managed();
            byte[] key1 = sha1.ComputeHash(hashBlock, 0, 64);
            byte[] key2 = sha1.ComputeHash(hashBlock, 64, 64);
            byte[] key = new byte[37];
            Array.Copy(key1, key, 20);
            Array.Copy(key2, 0, key, 20, 17);

            return key;
        }
        private static string CreateRandomModelNumber()
        {
            RNGCryptoServiceProvider randomSeedGenerator = new RNGCryptoServiceProvider();
            byte[] seedBuffer = new byte[4];
            randomSeedGenerator.GetBytes(seedBuffer);
            Random random = new Random(BitConverter.ToInt32(seedBuffer, 0));

            string _availableModelCharacters = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890";
            StringBuilder model = new StringBuilder(16);
            for (int i = 16; i > 0; i--)
            {
                model.Append(_availableModelCharacters[random.Next(_availableModelCharacters.Length)]);
            }

            return model.ToString();
        }
        public static byte[] ConvertToByteArray(string hex)
        {
            int len = hex.Length;
            byte[] bytes = new byte[len / 2];
            for (int i = 0; i < len; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }
            return bytes;
        }
        public static string ConvertToString(byte[] bytes)
        {
            return BitConverter.ToString(bytes).Replace("-", string.Empty);
        }
        #endregion
    }
}
